在昨天只有介紹了 extends,還有其他好用的關鍵字。
typeof 關鍵字可以用來取得一個變數或表達式的型別。
const user = {
  name: "John",
  age: 30,
};
type UserType = typeof user; // { name: string; age: number; }
keyof 可以取得一個物件的所有 key,然後組成 Union Type。
type User = {
  name: string;
  age: number;
};
type UserKeys = keyof User; // 'name' | 'age'
in 是映射型別(Mapped Types)的語法,用來遍歷型別的所有屬性鍵,並根據這些鍵來創建或修改型別。
type Permissions = "read" | "write" | "delete";
type PermissionFlags = { [P in Permissions]: boolean };
// { read: boolean; write: boolean; delete: boolean }
語法基本會長這樣
T extends U ? X : Y
T 和 U 是型別,T extends U 是條件判斷。如果 T 是 U 的子型別,則結果為 X。否則,結果為 Y。
範例:
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
條件型別也可以疊加使用,進行複雜的邏輯處理:
type Result<T> = T extends string
  ? { text: T }
  : T extends number
  ? { value: T }
  : never;
infer 需要搭配 extends 和 Condition Type 使用。它可以在條件型別中推斷出某個特定的型別變數。
type ElementType<T> = T extends (infer U)[] ? U : T;
infer U 會嘗試推斷陣列中的元素型別,並將其存儲在變數 U 中。
另外 infer 也經常會搭配 never 來使用,算是一種 TypeScript 表達錯誤的方式或是表示某個型別不應該存在。
了解上面的概念後,就可以了解 Utility Types 是怎麼運作的了,都是以上概念組合而已。
Utility Types 有很多,這邊介紹比較常用到的。
Exclude 移除某個 Union Type 的值type Exclude<T, U> = T extends U ? never : T;
type AllRoles = "admin" | "editor" | "viewer";
type ForbiddenRoles = "editor" | "viewer";
type AllowedRoles = Exclude<UserRoles, ForbiddenRoles>;
// "admin"
Extract 取出某個 Union Type 的值type Extract<T, U> = T extends U ? T : never;
type AllRoles = "admin" | "editor" | "viewer";
type ActiveRoles = "admin" | "viewer";
type ActiveUserRoles = Extract<AllRoles, ActiveRoles>;
// "admin" | "viewer"
Record 可以透過給 property 和 type 的方式創造 objecttype Record<K extends keyof any, T> = {
  [P in K]: T;
};
type UserPermissions = Record<"read" | "write" | "delete", boolean>;
Omit 可以移除 object 中某幾個 property 和 typetype Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P] };
Pick 可以只取 object 中某幾個 property 和 typetype Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};
Partial 把整個 object 的屬性改成可選的type Partial<T> = { [P in keyof T]?: T[P] | undefined };
範例:
type UserProfile = {
  name: string;
  age: number;
  email: string;
};
type PublicUserProfile = Omit<UserProfile, "email">;
// { name: string; age: number; }
type ContactInfo = Pick<UserProfile, "email">;
// { email: string; }
type UpdateUserProfile = Partial<UserProfile>;
// { name?: string; age?: number; email?: string; }
type ReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : any;
type Parameters<T extends (...args: any) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never;
範例:
function getUserInfo(userId: number): { name: string; age: number } {
  return {
    name: "John",
    age: 30,
  };
}
type UserInfo = ReturnType<typeof getUserInfo>;
// { name: string; age: number; }
type GetUserParams = Parameters<typeof getUserInfo>;
// [userId: number]
透過各種組合,也可以建立出自己的 Utility Types。
以下是一個範例,PickByValue 用來選擇型別中屬性值為指定型別的屬性:
type PickByValue<T, V> = Pick<
  T,
  { [K in keyof T]: T[K] extends V ? K : never }[keyof T]
>;
type User = {
  name: string;
  age: number;
};
type StringProperties = PickByValue<User, string>; // { name: string; }
參考資料:
https://www.typescriptlang.org/docs/handbook/utility-types.html#intrinsic-string-manipulation-types
https://stackoverflow.com/questions/60067100/why-is-the-infer-keyword-needed-in-typescript
https://www.youtube.com/watch?v=hLZXJTm7TEk
https://chentsulin.medium.com/typescript-infer-%E7%9A%84%E5%BC%B7%E5%A4%A7%E5%8A%9F%E7%94%A8-9b43c4eac6fb